(ns cljsso-client.middleware.sso
  (:require
   [compojure.core :refer [defroutes GET POST ANY]]
   [taoensso.timbre :as t]
   [cljsso-client.config :refer [props]]
   [noir.session :as session]
   [noir.response :as r]
   [sparrows.cypher :refer :all]
   [sparrows.misc :refer [trim]]
   [clojure.string :as s]

   [org.httpkit.client :as http]
   [clojure.data.json :as json]
   ))


(def ^:dynamic *request-id* nil)


(defonce ticket-store
  (atom {}))

(def ^:private sso-login-handlers
  (atom []))
(defn register-sso-login-handler
  "Add a function which will be executed when sso-login success. When
  multiple login-handlers are present, they will be executed in the
  order of their registration. `func` should be a function taking a map
  as parameter, e.g.,
  {:uid uid :skey skey :ticket ticket :userinfo userinfo}
  "
  [func]
  (swap! sso-login-handlers conj func))

;; Create a macro for all these url definitions
(defonce sso-login-url
  (or (get-in props [:sso-servers 0 :login-url])
      (throw (Exception. ":login-url not configured" ))))

(defonce sso-validate-url
  (or (get-in props [:sso-servers 0 :ticket-validate-url])
      (throw (Exception. ":login-url not configured" ))))

(defonce sso-logout-url
  (get-in props [:sso-servers 0 :logout-url]))

(defonce sso-client-url
  (str (:scheme props) "://" (:app-host props)))


(defn gen-uid-key
  [uid skey]
  (str uid ":" skey))

(defn get-url-from-req
  [req]
  (str
   (name (or (:scheme props) (:scheme req)))
   "://"
   (:server-name req)
   ".com"
   (:uri req)))

(defn get-requester-ip
  "Get real ip address of client"
  [req]
  (or (get-in req [:headers "x-real-ip"]) (:remote-addr req)))




(defonce private-key
  (:sso-key props))

(defn wrap-request-logger [handler]
  "Wrap a logger logging request uri, username, ip address and response
  time. A random *request-id* will be associated with every request."
  (fn [req]
    (let [now        (System/currentTimeMillis)
          request-id (or (:request-id req) (rand-int 0xffff))]
      (try
        (binding [*request-id* request-id]
          (handler (assoc req :request-id request-id)))
        (catch Exception e
          (t/error e (str request-id "Exception: " (.getMessage e) " on Req: " req ))
          (throw e))
        (finally
          (if (= :debug (:log-level props))
            (t/debug
             request-id
             " Session:" @session/*noir-session*
             " Req:" req
             " Time:" (- (System/currentTimeMillis) now))
            (t/info
             request-id
             " Admin:" (session/get :amdin)
             " User:" (session/get :user)
             " Req:" (req :uri)
             " From:" (or (get-in req [:headers "x-real-ip"]) (:remote-addr req))
             " Time:" (- (System/currentTimeMillis) now))))))))

(defn get-ticket-from-session
  []
  (session/get :ticket))

(defn get-ticket-from-request
  "Get ticket from param/cookie"
  [req]
  (get-in req [:params :ticket])
  ;; (or (get-in req [:cookies "ticket" :value]))
  )




(defn- try-decrypt
  "Try to decrypt the encrypted key, returns nil if exception is caught"
  [ticket key]
  (try
    (read-string (decrypt-aes ticket key))
    (catch Exception e
      (t/warn "Decrypt ticket:" ticket "failed with msg:" (.getMessage e)))))


(defn get-userinfo-from-ticket
  "Get userinfo from ticket. Returns nil if ticket is nil or invalid"
  [ticket]
  (when ticket
    (if-let [userinfo (try-decrypt ticket private-key)]
      userinfo)))

(defn- validate-ticket-with-server
  [ticket]
  (when-let [resp
             (try
               @(http/get sso-validate-url
                          {:query-params {:ticket ticket}
                           :insecure? (:insecure? props)})
               (catch Exception e (t/error "Failed to validate ticket" ticket "with server" sso-validate-url)))]
    (let [{:keys [code msg]} (try
                               (json/read-json (:body resp))
                               (catch Exception e (t/warn "parse json error, " resp)))]
      (or (= code 0)
          (do
            (t/info "Validate ticket" ticket "failed, " msg )
            nil)))))


(defn validate-ticket-and-save
  "Validate ticket in request. If it's valid, write to session"
  [req]
  (when-let [ticket (get-ticket-from-request req)]
    (when (validate-ticket-with-server ticket)
      (let [{:keys [uid skey] :as userinfo} (get-userinfo-from-ticket ticket)]
        (t/info "uid" uid "skey" skey)
        (when (and uid skey)
          (t/info "Found valid userinfo for" uid "with skey" skey)
          (session/put! :ticket ticket)
          (session/put! :userinfo userinfo)

          (swap! ticket-store assoc (gen-uid-key uid skey) ticket)
          (swap! ticket-store assoc ticket userinfo)

          (let [umap {:uid uid :skey skey :ticket ticket :userinfo userinfo}]
            (doseq [func @sso-login-handlers]
              (func umap)))


          ticket)))))




(defn redirect-sso-login
  [req]
  (t/info req)
  ;; TODO (get-url-from-req req) will get https://mn, i.e., nginx upstream
  (let [service (get-url-from-req req)]
    (r/redirect
     (str
      sso-login-url
      "?service="  (form-encode sso-client-url)))))

(defn ticket-in-session-valid?
  "Checks if current session contains a valid ticket. Clears the session
  if the ticket is invalid."
  []
  (when-let [ticket (get-ticket-from-session)]
    (if (get @ticket-store ticket)
      true
      (do
        (session/clear!)
        false))))

(defn sso-check-handler
  "Checks if current session contains a valid ticket. Clears the session
  if the ticket is invalid.
  This method can be used in a base access rule for lib-noir"
  [req]
  (ticket-in-session-valid?))



(defn wrap-sso-check
  "Put ticket into session if a valid ticket is found in request param"
  [handler]
  (fn [req]
    (when-not (ticket-in-session-valid?)
      (validate-ticket-and-save req))
    (handler req)))


(defonce sso-client-rule
  {:rules [sso-check-handler]
   :on-fail (fn [req]
              (redirect-sso-login req))})

(defonce ^:private logout-handlers
  (atom #{}))

(defonce allowed-sso-server-ips
  (conj (apply hash-set (map :ip (:sso-servers props)))
        "127.0.0.1" ))

(defn validate-sso-server
  "TODO Check if this request is from an address in the allowed list of
  SSO servers"
  [req]
  (let [ip (get-requester-ip req)]
    (or (some (partial = ip) allowed-sso-server-ips)
        (do (t/warn "SSO-serv req" (:uri req) "from" ip "not authorized!")
            nil))))

(defn sso-logout-handler
  "Logging out this uid. Called by sso-server
  should validate the source of this request"
  [{:keys [params] :as req}]
  (when (validate-sso-server req)
    (let [{:keys [uid skey]} params]
      (r/json
       (if-let [ticket (get @ticket-store (str uid ":" skey))]
         (do
           (t/debug "Logging out session with ticket" ticket  "for" uid "with skey" skey)
           (swap! ticket-store dissoc (gen-uid-key uid skey))
           (swap! ticket-store dissoc ticket)
           {:code 0})
         {:code 2 :msg "Session not found!"})))))
